﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Administration;
using System.IO;
using Microsoft.SharePoint;
using System.Collections.ObjectModel;
using System.Threading;

namespace uk.ac.soton.ses
{
    //Please note that this project needs to be built to target x64 otherwise a null reference is returned when the SPFarm is accessed


    public static class SharePointDeploymentTools
    {
        public static bool CopyWSP(string pathToWSP, bool replaceExisting)
        {
            //Get the local SharePoint farm
            SPFarm farm = SPFarm.Local;

            //Start the copy to the local farm
            return SharePointDeploymentTools.CopyWSP(pathToWSP, replaceExisting, farm);
        }

        public static int RemoveFeatureFromCollection(string featureName, SPFeatureCollection collection)
        {
            List<SPFeature> featuresToRemove = new List<SPFeature>();

            //Populate a list of features to remove from the collection
            foreach (SPFeature feature in collection)
            {
                //Some features exist but don't have a definition so we can't find their name.
                //In this case we'll just have to skip it as we're not able to match it
                if (feature.Definition == null)
                {
                    continue;
                }
                else if (feature.Definition.DisplayName == featureName)
                {
                    featuresToRemove.Add(feature);
                }
            }

            //Remove the features that matched the name from the collection
            foreach (SPFeature featureToRemove in featuresToRemove)
            {
                collection.Remove(featureToRemove.Definition.Id);
            }
            return featuresToRemove.Count;
        }

        private static bool FeatureExists(string featureName, SPFeatureCollection collection)
        {
            bool exists = false;
            foreach (SPFeature feature in collection)
            {
                if (feature.Definition != null && feature.Definition.DisplayName == featureName)
                {
                    exists = true;
                    break;
                }
            }
            return exists;
        }

        private static bool WebApplicationExistsAtAddress(Uri deployUrl)
        {
            SPWebApplication webappForWeb = SPWebApplication.Lookup(deployUrl);
            return (webappForWeb != null);
        }

        public static bool ActivateFeature(string featureName, Uri deployUrl, SPFarm farm)
        {
            //Get the feature requested
            SPFeatureDefinition featureDefinition = farm.FeatureDefinitions[featureName];
            if (featureDefinition != null)
            {
                SPFeatureScope featureScope = featureDefinition.Scope;
                SPFeatureCollection collection = null;
                bool exists = false;
                //Get the specified location
                switch (featureScope)
                {
                    case SPFeatureScope.Farm:
                        //Get SPWebService.ContentService
                        SPWebService contentService = farm.Services.GetValue<SPWebService>();
                        if (contentService != null)
                        {
                            collection = contentService.Features;
                        }
                        break;

                    case SPFeatureScope.WebApplication:
                        SPWebApplication webapp = SPWebApplication.Lookup(deployUrl);
                        if (webapp != null)
                        {
                            collection = webapp.Features;
                        }
                        break;

                    case SPFeatureScope.Site:
                        //Check there's a web application at the address specified in the farm or we'll get exceptions
                        if (SharePointDeploymentTools.WebApplicationExistsAtAddress(deployUrl))
                        {
                            using (SPSite site = new SPSite(deployUrl.ToString()))
                            {
                                if (site != null)
                                {
                                    collection = site.Features;
                                }
                            }
                        }
                        break;

                    case SPFeatureScope.Web:
                        //Check there's a web application at the address specified in the farm or we'll get exceptions
                        if (SharePointDeploymentTools.WebApplicationExistsAtAddress(deployUrl))
                        {
                            using (SPSite site = new SPSite(deployUrl.ToString()))
                            {
                                if (site != null)
                                {
                                    using (SPWeb web = site.OpenWeb())
                                    {
                                        if (web != null)
                                        {
                                            collection = web.Features;
                                        }
                                    }
                                }
                            }
                        }
                        break;
                }

                //Check we found a collection of features to modify
                if (collection != null)
                {
                    //Check whether feature exists already in collection
                    exists = SharePointDeploymentTools.FeatureExists(featureDefinition.DisplayName, collection);
                    if (!exists)
                    {
                        //If it doesn't exist, activate the feature
                        collection.Add(featureDefinition.Id, true);
                        Console.WriteLine("Activated {0} in {1} {2}", featureName, featureScope, deployUrl.ToString());

                        //Return success
                        return true;
                    }
                    else
                    {
                        Console.WriteLine("{0} was already active", featureName);

                        //If we were asked to activate an already active feature, we can return success because it is not considered to be an error
                        return true;
                    }
                }
                else
                {
                    Console.WriteLine("collection was null");

                    //Return failure
                    return false;
                }
            }
            else
            {
                Console.WriteLine(String.Format("Feature definition {0} was not found", featureName));

                //Return failure
                return false;
            }


        }

        //I am not accepting a scope as a parameter at the moment because I only need that if Name is not unique and I have not been able
        //to deploy features with non-unique names so I am assuming that I don't need more information to locate the feature other than the name
        public static int DeactivateFeature(string featureName, SPFarm farm)
        {
            int numberDeleted = 0;

            SPWebServiceCollection serviceCollection = new SPWebServiceCollection(farm);
            SPFeatureDefinition featureDefinition = farm.FeatureDefinitions[featureName];
            if (featureDefinition != null)
            {
                SPFeatureScope featureScope = featureDefinition.Scope;

                //Iterate around all of the levels of SharePoint: Farm>WebApps>Sites>Webs
                //Remove the requested feature when found at the specified level
                //There must be a better way to do this as this code is hard to read!

                //Farm level
                foreach (SPWebService service in serviceCollection)
                {
                    if (featureScope == SPFeatureScope.Farm)
                    {
                        numberDeleted += SharePointDeploymentTools.RemoveFeatureFromCollection(featureName, service.Features);
                    }
                    else
                    {
                        //Web application level (IIS)
                        foreach (SPWebApplication webapp in service.WebApplications)
                        {
                            if (featureScope == SPFeatureScope.WebApplication)
                            {
                                numberDeleted += SharePointDeploymentTools.RemoveFeatureFromCollection(featureName, webapp.Features);
                            }
                            else
                            {
                                //SPSite level (aka site collection)
                                foreach (SPSite site in webapp.Sites)
                                {
                                    if (featureScope == SPFeatureScope.Site)
                                    {
                                        numberDeleted += SharePointDeploymentTools.RemoveFeatureFromCollection(featureName, site.Features);
                                    }
                                    else
                                    {
                                        //SPWeb level (aka sub site)
                                        foreach (SPWeb web in site.AllWebs)
                                        {
                                            if (featureScope == SPFeatureScope.Web)
                                            {
                                                numberDeleted += SharePointDeploymentTools.RemoveFeatureFromCollection(featureName, web.Features);
                                            }
                                        } //foreach AllWebs
                                    } //else
                                } //foreach Sites
                            } //else
                        } //foreach WebApplications
                    } //else
                } //foreach serviceCollection
            }
            return numberDeleted;
        }

        public static bool CopyWSP(string pathToWSP, bool replaceExisting, SPFarm farm)
        {
            return SharePointDeploymentTools.CopyWSP(pathToWSP, replaceExisting, farm, null);
        }

        public static bool CopyWSP(string pathToWSP, bool replaceExisting, SPFarm farm, Collection<SPWebApplication> webapps)
        {
            if (!File.Exists(pathToWSP))
            {
                Console.WriteLine("{0} not found");
                return false;
            }

            if (farm != null)
            {

                //Get the requested solution (to check if it already exists because we may need to replace it)
                string wspName = new FileInfo(pathToWSP).Name;
                Console.WriteLine(string.Format("wsp:{0}", wspName));
                SPSolution solution = farm.Solutions[wspName];

                if (solution != null) //WSP already exists in farm
                {
                    if (!replaceExisting)
                    {
                        Console.WriteLine("WSP already exists");
                        //We weren't asked to replace an existing solution so nothing more to do
                        return false;
                    }
                    else
                    {
                        //Retract solution from the web applications where it has been deployed
                        SharePointDeploymentTools.RetractSolution(solution);
                        //Remove it
                        farm.Solutions.Remove(solution.Id);
                    }
                }
                
                //We are sure now that the WSP does not exist in farm (possibly by removing an existing one)
                //Add the new WSP
                SPSolution newSolution = farm.Solutions.Add(pathToWSP);
                if (newSolution.ContainsWebApplicationResource && webapps != null)
                {
                    newSolution.Deploy(DateTime.Now, newSolution.ContainsGlobalAssembly, webapps, false);
                    SharePointDeploymentTools.WaitForTimerJob(newSolution.Name, farm);
                }
                if (!newSolution.ContainsWebApplicationResource && newSolution.ContainsGlobalAssembly)
                {
                    newSolution.Deploy(DateTime.Now, true, false);
                    SharePointDeploymentTools.WaitForTimerJob(newSolution.Name, farm);
                }

                //Return success
                return true;
            }
            else
            {
                Console.WriteLine("Farm not found");
                return false;
            }
        }

        public static void RetractSolution(SPSolution solution)
        {
            SPFarm farm = solution.Farm;

            //We'll remove it from the web applications it's deployed to, but
            //if you check SPSolution.DeployedWebApplications when the solution is a site level solution
            //a bug within SharePoint causes an exception even if you are checking for null
            //(as SharePoint tries to create a Guid using a name).
            //Checking whether it's deployed and where to first avoids this issue
            if (
                solution.ContainsWebApplicationResource
                && (solution.DeploymentState == SPSolutionDeploymentState.WebApplicationDeployed
                || solution.DeploymentState == SPSolutionDeploymentState.GlobalAndWebApplicationDeployed)
                && solution.DeployedWebApplications != null)
            {
                //Start a timer job to retract the solution from the web application
                solution.Retract(DateTime.Now, solution.DeployedWebApplications);
            }
            else if (
                !solution.ContainsWebApplicationResource
                && (solution.DeploymentState == SPSolutionDeploymentState.GlobalDeployed
                || solution.DeploymentState == SPSolutionDeploymentState.GlobalAndWebApplicationDeployed)
                && solution.DeployedServers != null
                && solution.DeployedServers.Count > 0)
            {
                //Start a timer job to retract the solution from the servers on which it is deployed
                solution.Retract(DateTime.Now);
            }
            else
            {
                Console.WriteLine("Nothing to retract.");
                return;
            }

            //Wait for the timer job to complete
            SharePointDeploymentTools.WaitForTimerJob(solution.Name, farm);

        }

        private static void WaitForTimerJob(string timerJobSearchString, SPFarm farm)
        {
            bool timerJobExists = true;
            while (timerJobExists)
            {
                Thread.Sleep(1000);
                SPJobDefinitionCollection jobs = farm.TimerService.JobDefinitions;
                timerJobExists = false;

                foreach (SPJobDefinition job in jobs)
                {
                    if (job.Name.Contains(timerJobSearchString))
                    {
                        timerJobExists = true;
                        break;
                    }
                }
            }
        }
    }
}
